/*
  Copyright 2012 Sergey Ostanin

  Licensed under the Apache License, Version 2.0 (the "License");
  you may not use this file except in compliance with the License.
  You may obtain a copy of the License at

      http://www.apache.org/licenses/LICENSE-2.0

  Unless required by applicable law or agreed to in writing, software
  distributed under the License is distributed on an "AS IS" BASIS,
  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  See the License for the specific language governing permissions and
  limitations under the License.
*/

package ru.irenproject;

import static ru.irenproject.it.XmlUtils.getChildren;

import java.io.IOException;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;

import javax.inject.Inject;

import org.apache.commons.lang3.Validate;
import org.w3c.dom.Element;

import ru.irenproject.it.It;
import ru.irenproject.it.ItConsumer;
import ru.irenproject.it.ItInput;
import ru.irenproject.it.ItOutput;
import ru.irenproject.it.ItProducer;
import ru.irenproject.it.ItReader;
import ru.irenproject.it.ItWriter;
import ru.irenproject.store.AbstractThing;
import ru.irenproject.store.AspectId;
import ru.irenproject.store.Event;
import ru.irenproject.store.Initializer;
import ru.irenproject.store.Kind;
import ru.irenproject.store.Stash;
import ru.irenproject.store.Thing;

import com.google.common.collect.Lists;
import com.google.protobuf.ByteString;
import com.google.protobuf.Message;

public final class QuestionListEditor extends AbstractThing implements Visual {
  public enum SelectItemEvent implements Event { INSTANCE }

  public static final class SelectInput {
    public int[] indices;
  }

  public static final class AddInput {
    public String kind;
    public String initialText;
  }

  public static final class SetWeightInput {
    public int weight;
  }

  public static final class EnableInput {
    public boolean enable;
  }

  public static final class MoveInput {
    public boolean forward;
  }

  private static final AspectId ASPECT_ID = new AspectId(0x289c412acc457f7bL);

  public static QuestionListEditor open(final QuestionList questionList) {
    return Utils.requireAspect(questionList, ASPECT_ID, QuestionListEditor.class, new Initializer<QuestionListEditor>() {
      @Override public void initialize(QuestionListEditor target) {
        target.prepare(questionList);
      }
    });
  }

  @Stash private UiProtos.QuestionListEditor.Builder $;
  private final ItOutput fOutput;
  private final ItInput fInput;
  private final ProtoRegistry fProtoRegistry;

  @Inject QuestionListEditor(ItOutput itOutput, ItInput itInput,
      ProtoRegistry protoRegistry) {
    fOutput = itOutput;
    fInput = itInput;
    fProtoRegistry = protoRegistry;
  }

  private void prepare(QuestionList questionList) {
    $.setQuestionList(questionList.id());
    $.setSelection(QuestionListSelection.create(this).id());
    questionList.addListener(this);

    if (!questionList.isEmpty()) {
      selection().add(questionList.itemIds(0));
    }

    update();
  }

  public QuestionList questionList() {
    return get($.getQuestionList(), QuestionList.class);
  }

  public List<ByteString> selectedItemIdList() {
    return selection().list();
  }

  public List<QuestionItem> selectedItems() {
    return store().getBatch(selectedItemIdList(), QuestionItem.class);
  }

  public boolean hasSelectedItems() {
    return selection().count() > 0;
  }

  public QuestionItem activeItem() {
    return (selection().count() == 1) ?
        get(selection().get(0), QuestionItem.class) : null;
  }

  @Action(badInput = TestInputException.class, markAsModified = false)
  public void doSelect(SelectInput in) {
    Check.inputNotNull(in.indices);

    int last = -1;
    QuestionList questionList = questionList();
    selection().clear();
    for (int index : in.indices) {
      Check.input(index > last);
      last = index;
      Check.input(index < questionList.itemCount());
      selection().add(questionList.itemIds(index));
    }

    post(SelectItemEvent.INSTANCE);
  }

  @Override public Message render() {
    return UiProtos.QuestionListEditorShape.newBuilder()
        .addAllItemEditor($.getItemEditorList())
        .setSelection($.getSelection())
        .setMaxWeight(QuestionItem.MAX_WEIGHT)
        .build();
  }

  @Override protected void handleEvent(Event e, Thing source) {
    if (e instanceof QuestionList.ChangeEvent) {
      update();
    }
  }

  private void update() {
    List<ByteString> sel = new ArrayList<>(selection().list());
    int selectIndex = sel.isEmpty() ?
        -1 : indexOfItemId(sel.get(sel.size() - 1)) + 1;
    ByteString selectItem = ((selectIndex != -1) && (selectIndex < $.getItemCount())) ?
        $.getItem(selectIndex) : null;

    sync();

    LinkedHashSet<ByteString> items = new LinkedHashSet<>(questionList().itemIdList());
    if (sel.retainAll(items)) {
      if (sel.isEmpty()) {
        if ((selectItem != null) && items.contains(selectItem)) {
          sel.add(selectItem);
        } else {
          selectIndex = Math.min(selectIndex, items.size() - 1);
          if (selectIndex != -1) {
            sel.add($.getItem(selectIndex));
          }
        }
      }
      setSelectedItemIds(sel);
    }

    selection().update();
  }

  private void sync() {
    List<ByteString> oldItems = $.getItemList();
    List<ByteString> oldEditors = $.getItemEditorList();

    Map<ByteString, ByteString> oldItemToEditorMap = new HashMap<>();
    int n = oldItems.size();
    for (int i = 0; i < n; ++i) {
      oldItemToEditorMap.put(oldItems.get(i), oldEditors.get(i));
    }

    List<ByteString> items = questionList().itemIdList();
    List<ByteString> editors = new ArrayList<>();

    for (ByteString item : items) {
      ByteString editor = oldItemToEditorMap.get(item);
      if (editor == null) {
        editor = QuestionItemEditor.create(get(item, QuestionItem.class)).id();
      }
      editors.add(editor);
    }

    $.clearItem().addAllItem(items);
    $.clearItemEditor().addAllItemEditor(editors);
  }

  private int indexOfItemId(ByteString itemId) {
    return $.getItemList().indexOf(itemId);
  }

  private void setSelectedItems(Iterable<QuestionItem> items) {
    setSelectedItemIds(Utils.getThingIds(items));
  }

  private void setSelectedItemIds(Iterable<ByteString> itemIds) {
    selection().clear();
    selection().addAll(itemIds);
    post(SelectItemEvent.INSTANCE);
  }

  private void setSelectedItem(QuestionItem item) {
    setSelectedItems(Collections.singleton(item));
  }

  @Action(markAsModified = false)
  public void doCopy() {
    copy();
  }

  private void copy() {
    try {
      if (hasSelectedItems()) {
        try (OutputStream clipboard = Files.newOutputStream(clipboardFile())) {
          fOutput.writeFragment(new ItProducer() {
            @Override public void produce(ItWriter out) {
              for (QuestionItem item : selectedItems()) {
                out.writeQuestionItem(item);
              }
            }
          }, clipboard);
        }
      }
    } catch (IOException e) {
      throw new RuntimeException(e);
    }
  }

  private Path clipboardFile() {
    return DataDirectory.get().resolve("clipboard");
  }

  @Action public void doPaste() {
    paste();
  }

  public void paste() {
    Path clipboard = clipboardFile();
    if (Files.exists(clipboard)) {
      selection().clear();

      fInput.read(clipboard, store(), new ItConsumer() {
        @Override public void consume(ItReader in) {
          List<Element> questions = getChildren(in.root(), It.ELEM_QUESTION);
          for (Element xmlQuestion : questions) {
            QuestionItem item = in.readQuestionItem(xmlQuestion);
            questionList().addItem(item);
            selection().add(item.id());
          }
        }
      });

      post(SelectItemEvent.INSTANCE);
    }
  }

  @Action public void doDelete() {
    delete();
  }

  private void delete() {
    questionList().removeItems(selectedItems());
  }

  @Action public void doCut() {
    copy();
    delete();
  }

  @Action(badInput = TestInputException.class)
  public void doAdd(AddInput in) {
    Kind kind = Kind.fromAlienHexString(in.kind);
    Class<? extends AbstractThing> questionClass =
        Check.inputNotNull(fProtoRegistry.getThingClassOrNull(kind));
    Check.input(Question.class.isAssignableFrom(questionClass));

    Question question = (Question) store().create(questionClass);
    question.setDefaults(Check.inputNotNull(in.initialText));
    QuestionItem item = QuestionItem.create(question);
    questionList().addItem(item);
    setSelectedItem(item);
  }

  @Action(badInput = TestInputException.class)
  public void doSetWeight(SetWeightInput in) {
    for (QuestionItem item : selectedItems()) {
      item.setWeight(in.weight);
    }
  }

  @Action public void doEnable(EnableInput in) {
    for (QuestionItem item : selectedItems()) {
      item.setEnabled(in.enable);
    }
  }

  @Action(badInput = TestInputException.class)
  public void doMove(MoveInput in) {
    List<QuestionItem> sel = selectedItems();
    if (in.forward) {
      sel = Lists.reverse(sel);
    }

    Map<ByteString, Integer> indexMap = questionList().getItemIdToIndexMap();

    for (QuestionItem item : sel) {
      questionList().moveItem(Validate.notNull(indexMap.get(item.id())), in.forward);
    }
  }

  private QuestionListSelection selection() {
    return get($.getSelection(), QuestionListSelection.class);
  }
}
